// SPDX-License-Identifier: Mulan PSL v2
/*
 * Copyright (c) 2025 Huawei Technologies Co., Ltd.
 * This software is licensed under Mulan PSL v2.
 * You can use this software according to the terms and conditions of the Mulan PSL v2.
 * You may obtain a copy of Mulan PSL v2 at:
 *         http://license.coscl.org.cn/MulanPSL2
 *
 * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
 * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
 * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
 * See the Mulan PSL v2 for more details.
 */

use std::{
    io,
    os::fd::{AsFd, BorrowedFd},
};

use rustix::fs;

pub use fs::FlockOperation as FileLockOperation;

/// A utility for advisory file locking via `flock`.
pub struct FileLock;

impl FileLock {
    #[inline]
    /// Acquires a lock on the file.
    pub fn lock<F: AsFd>(file: &F, op: FileLockOperation) -> io::Result<FileLockGuard<'_>> {
        let fd = file.as_fd();
        fs::flock(fd, op)?;

        Ok(FileLockGuard(fd))
    }

    /// Acquires an exclusive lock on the file.
    ///
    /// Blocks until the lock is acquired.
    #[inline]
    pub fn exclusive<F: AsFd>(file: &F) -> io::Result<FileLockGuard<'_>> {
        Self::lock(file, FileLockOperation::LockExclusive)
    }

    /// Acquires a shared lock on the file.
    ///
    /// Blocks until the lock is acquired.
    #[inline]
    pub fn shared<F: AsFd>(file: &F) -> io::Result<FileLockGuard<'_>> {
        Self::lock(file, FileLockOperation::LockShared)
    }

    /// Attempts to acquire an exclusive lock without blocking.
    ///
    /// Returns `Err(io::ErrorKind::WouldBlock)` if the lock cannot be acquired immediately.
    #[inline]
    pub fn try_exclusive<F: AsFd>(file: &F) -> io::Result<FileLockGuard<'_>> {
        Self::lock(file, FileLockOperation::NonBlockingLockExclusive)
    }

    /// Attempts to acquire a shared lock without blocking.
    ///
    /// Returns `Err(io::ErrorKind::WouldBlock)` if the lock cannot be acquired immediately.
    #[inline]
    pub fn try_shared<F: AsFd>(file: &F) -> io::Result<FileLockGuard<'_>> {
        Self::lock(file, FileLockOperation::NonBlockingLockShared)
    }
}

/// A RAII guard that automatically releases the lock when dropped.
///
/// The underlying file descriptor remains accessible via `AsFd`.
#[derive(Debug)]
#[repr(transparent)]
pub struct FileLockGuard<'a>(BorrowedFd<'a>);

impl AsFd for FileLockGuard<'_> {
    #[inline]
    fn as_fd(&self) -> BorrowedFd<'_> {
        self.0
    }
}

impl Drop for FileLockGuard<'_> {
    fn drop(&mut self) {
        // Best-effort unlock, ignoring errors in drop is standard practice.
        let _ = fs::flock(self.0, FileLockOperation::Unlock);
    }
}

#[cfg(test)]
mod tests {
    use std::{
        env, fs,
        io::ErrorKind,
        path::PathBuf,
        process,
        sync::{Arc, Barrier},
        thread,
        time::Duration,
    };

    use super::*;

    struct TempFile {
        path: PathBuf,
        file: Option<fs::File>,
    }

    impl TempFile {
        fn new(name: &str) -> Self {
            let mut path = env::temp_dir();
            path.push(format!("flock_test_{}_{}", process::id(), name));

            Self {
                path: path.clone(),
                file: Some(fs::File::create(&path).unwrap()),
            }
        }

        fn file(&self) -> &fs::File {
            self.file.as_ref().unwrap()
        }
    }

    impl Drop for TempFile {
        fn drop(&mut self) {
            self.file = None;
            let _ = fs::remove_file(&self.path);
        }
    }

    #[test]
    fn test_exclusive_conflict() {
        let temp_file = TempFile::new("exclusive_conflict");
        let other_file_handle = fs::File::open(&temp_file.path).unwrap();

        // Acquire an exclusive lock on the first handle
        let lock_guard = FileLock::exclusive(temp_file.file()).unwrap();

        // Verify that a second handle cannot acquire an exclusive lock
        assert!(matches!(
            FileLock::try_exclusive(&other_file_handle)
                .unwrap_err()
                .kind(),
            ErrorKind::WouldBlock
        ));

        // Release the lock and verify the second handle can now acquire it
        drop(lock_guard);
        assert!(FileLock::try_exclusive(&other_file_handle).is_ok());
    }

    #[test]
    fn test_shared_coexistence() {
        let temp_file = TempFile::new("shared_coexistence");
        let other_file_handle = fs::File::open(&temp_file.path).unwrap();

        // Acquire a shared lock on the first handle
        let _shared_guard = FileLock::shared(temp_file.file()).unwrap();

        // Verify that a second handle can also acquire a shared lock
        assert!(FileLock::try_shared(&other_file_handle).is_ok());

        // Verify that a second handle cannot acquire an exclusive lock while shared lock exists
        assert!(matches!(
            FileLock::try_exclusive(&other_file_handle)
                .unwrap_err()
                .kind(),
            ErrorKind::WouldBlock
        ));
    }

    #[test]
    fn test_blocking_behavior() {
        let temp_file = TempFile::new("blocking");
        let file_path = temp_file.path.clone();

        let barrier = Arc::new(Barrier::new(2));
        let thread_barrier = barrier.clone();

        // Main thread holds the exclusive lock
        let lock_guard = FileLock::exclusive(temp_file.file()).unwrap();

        let join_handle = thread::spawn(move || {
            let thread_file_handle = fs::File::open(file_path).unwrap();

            // Sync with main thread to ensure the lock is already held
            thread_barrier.wait();

            // This call should block until the main thread drops its lock
            let _thread_guard = FileLock::exclusive(&thread_file_handle).unwrap();
        });

        // Wait for the thread to start and open the file
        barrier.wait();

        // Sleep briefly to ensure the thread enters the blocking syscall
        thread::sleep(Duration::from_millis(50));

        // Release the lock, allowing the thread to proceed
        drop(lock_guard);

        join_handle.join().unwrap();
    }
}
